/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 *    Expression.java
 *    Copyright (C) 2008-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.core.pmml;

import java.io.Serializable;
import java.util.ArrayList;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import weka.core.Attribute;

public abstract class Expression implements Serializable {

    /**
     * For serialization
     */
    private static final long serialVersionUID = 4448840549804800321L;

    /** The optype of this Expression */
    protected FieldMetaInfo.Optype m_opType;

    /** The field defs */
    protected ArrayList<Attribute> m_fieldDefs = null;

    // NOTE - might need to pass in mining schema in order
    // to determine values for nominal optypes
    public Expression(FieldMetaInfo.Optype opType, ArrayList<Attribute> fieldDefs) {
        m_opType = opType;
        m_fieldDefs = fieldDefs;
    }

    /**
     * Set the field definitions for this Expression to use
     * 
     * @param fieldDefs the field definitions to use
     * @throws Exception if there is a problem setting the field definitions
     */
    public void setFieldDefs(ArrayList<Attribute> fieldDefs) throws Exception {
        m_fieldDefs = fieldDefs;
    }

    /**
     * Get the result of evaluating the expression. In the case of a continuous
     * optype, a real number is returned; in the case of a categorical/ordinal
     * optype, the index of the nominal value is returned as a double.
     * 
     * @param incoming the incoming parameter values
     * @return the result of evaluating the expression
     * @throws Exception if there is a problem computing the result
     */
    public abstract double getResult(double[] incoming) throws Exception;

    /**
     * Get the result of evaluating the expression for continuous optype. Is the
     * same as calling getResult() when the optype is continuous.
     * 
     * @param incoming the incoming parameter values mining schema
     * @return the result of evaluating the expression.
     * @throws Exception if the optype is not continuous.
     */
    public double getResultContinuous(double[] incoming) throws Exception {
        if (!(m_opType == FieldMetaInfo.Optype.CONTINUOUS)) {
            throw new Exception("[Expression] Can't return continuous result " + "as optype is not continuous");
        }
        return getResult(incoming);
    }

    /**
     * Gets the result of evaluating the expression when the optype is categorical
     * or ordinal as the actual String value.
     * 
     * @param incoming the incoming parameter values
     * @return the result of evaluating the expression
     * @throws Exception if the optype is continuous
     */
    public abstract String getResultCategorical(double[] incoming) throws Exception;

    /**
     * Return the structure of the result of applying this Expression as an
     * Attribute.
     * 
     * @return the structure of the result of applying this Expression as an
     *         Attribute.
     */
    protected abstract Attribute getOutputDef();

    /**
     * Static factory method that returns a subclass of Expression that encapsulates
     * the type of expression contained in the Element supplied. Assumes that there
     * is just one expression contained in the supplied Element.
     * 
     * @param container the Node containing the expression
     * @param opType    the optype of the value returned by this Expression.
     * @param fieldDefs an ArrayList of Attributes for the fields that this
     *                  Expression may need to access Since Expressions are children
     *                  of either DerivedFields or DefineFuntions, they will have
     *                  the same optype as their parent.
     * @param transDict the TransformationDictionary (may be null if there is no
     *                  dictionary)
     * @return an Expression object or null if there is no known expression in the
     *         container
     * @throws Exception for unsupported Expression types
     */
    public static Expression getExpression(Node container, FieldMetaInfo.Optype opType, ArrayList<Attribute> fieldDefs, TransformationDictionary transDict) throws Exception {

        // we need to examine children of this Node to find an expression,
        // not the entire subtree (as would be returned by
        // Element.getElementsByTagName()

        Expression result = null;
        String tagName = "";

        NodeList children = container.getChildNodes();
        if (children.getLength() == 0) {
            throw new Exception("[Expression] container has no children!");
        }

        // at this level in the tree there should be only one expression type
        // specified - look for it here.
        for (int i = 0; i < children.getLength(); i++) {
            Node child = children.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                tagName = ((Element) child).getTagName();
                result = getExpression(tagName, child, opType, fieldDefs, transDict);
                if (result != null) {
                    break;
                }
            }
        }

        return result;
    }

    /**
     * Static factory method that returns a subclass of Expression that encapsulates
     * the type of expression supplied as an argument.
     * 
     * @param name       the name of the Expression to get
     * @param expression the Node containing the expression
     * @param opType     the optype of the value returned by this Expression.
     * @param fieldDefs  an ArrayList of Attributes for the fields that this
     *                   Expression may need to access Since Expressions are
     *                   children of either DerivedFields or DefineFuntions, they
     *                   will have the same optype as their parent.
     * @param transDict  the TransformationDictionary (may be null if there is no
     *                   dictionary)
     * @return an Expression object or null if there is no known expression in the
     *         container
     * @throws Exception for unsupported Expression types
     */
    public static Expression getExpression(String name, Node expression, FieldMetaInfo.Optype opType, ArrayList<Attribute> fieldDefs, TransformationDictionary transDict) throws Exception {

        Expression result = null;

        if (name.equals("Constant")) {
            // construct a Constant expression
            result = new Constant((Element) expression, opType, fieldDefs);
        } else if (name.equals("FieldRef")) {
            // construct a FieldRef expression
            result = new FieldRef((Element) expression, opType, fieldDefs);
        } else if (name.equals("Apply")) {
            // construct an Apply expression
            result = new Apply((Element) expression, opType, fieldDefs, transDict);
        } else if (name.equals("NormDiscrete")) {
            result = new NormDiscrete((Element) expression, opType, fieldDefs);
        } else if (name.equals("NormContinuous")) {
            result = new NormContinuous((Element) expression, opType, fieldDefs);
        } else if (name.equals("Discretize")) {
            result = new Discretize((Element) expression, opType, fieldDefs);
        } else if (name.equals("MapValues") || name.equals("Aggregate")) {
            throw new Exception("[Expression] Unhandled Expression type " + name);
        }
        return result;
    }

    /**
     * Return the named attribute from the list of reference fields.
     * 
     * @param attName the name of the attribute to retrieve
     * @return the named attribute (or null if it can't be found).
     */
    public Attribute getFieldDef(String attName) {
        Attribute returnV = null;
        for (int i = 0; i < m_fieldDefs.size(); i++) {
            if (m_fieldDefs.get(i).name().equals(attName)) {
                returnV = m_fieldDefs.get(i);
                break;
            }
        }
        return returnV;
    }

    public int getFieldDefIndex(String attName) {
        int returnV = -1;
        for (int i = 0; i < m_fieldDefs.size(); i++) {
            if (m_fieldDefs.get(i).name().equals(attName)) {
                returnV = i;
                break;
            }
        }
        return returnV;
    }

    /**
     * Get the optype of the result of applying this Expression.
     * 
     * @return the optype of the result of applying this Expression
     */
    public FieldMetaInfo.Optype getOptype() {
        return m_opType;
    }

    public String toString() {
        return toString("");
    }

    public String toString(String pad) {
        return pad + this.getClass().getName();
    }
}
